{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "autostart-slider", "type": "registry:component", "dependencies": [ "embla-carousel", "lucide-react", "embla-carousel-class-names", "embla-carousel-react", "embla-carousel-autoplay", "motion" ], "devDependencies": [], "registryDependencies": [], "files": [ { "path": "./registry/components/carousel/autostart-slider.tsx", "content": "'use client';\r\nimport React, { ReactNode } from 'react';\r\nimport { EmblaOptionsType } from 'embla-carousel';\r\nimport {\r\n Carousel,\r\n Slider,\r\n SliderContainer,\r\n SliderDotButton,\r\n} from '@/components/ui/carousel';\r\nimport Image from 'next/image';\r\nimport { imgPreview } from '@/components/website/constant';\r\nimport Autoplay from 'embla-carousel-autoplay';\r\n\r\nfunction index() {\r\n const OPTIONS: EmblaOptionsType = { loop: true };\r\n return (\r\n <>\r\n \r\n \r\n \r\n
{' '}\r\n {/* Red background with rounded corners */}\r\n
\r\n \r\n
{' '}\r\n {/* Blue background with rounded corners */}\r\n
\r\n \r\n
{' '}\r\n {/* Green background with rounded corners */}\r\n
\r\n \r\n
{' '}\r\n {/* Yellow background with rounded corners */}\r\n
\r\n
\r\n
\r\n \r\n
\r\n \r\n \r\n );\r\n}\r\n\r\nexport default index;\r\n", "type": "registry:component" }, { "path": "./components/ui/carousel.tsx", "content": "'use client';\r\n\r\nimport React, {\r\n createContext,\r\n useCallback,\r\n useContext,\r\n useEffect,\r\n useId,\r\n useRef,\r\n useState,\r\n forwardRef,\r\n} from 'react';\r\nimport { motion, AnimatePresence } from 'motion/react';\r\nimport type {\r\n EmblaCarouselType,\r\n EmblaEventType,\r\n EmblaOptionsType,\r\n} from 'embla-carousel';\r\nimport useEmblaCarousel from 'embla-carousel-react';\r\nimport { cn } from '@/lib/utils';\r\n\r\n// ============= TYPES =============\r\ninterface CarouselProps extends React.HTMLAttributes {\r\n options?: EmblaOptionsType;\r\n plugins?: Parameters[1];\r\n isScale?: boolean;\r\n}\r\n\r\ninterface CarouselContextType {\r\n emblaApi: EmblaCarouselType | undefined;\r\n emblaThumbsApi: EmblaCarouselType | undefined;\r\n emblaRef: ReturnType[0];\r\n emblaThumbsRef: ReturnType[0];\r\n prevBtnDisabled: boolean;\r\n nextBtnDisabled: boolean;\r\n onPrevButtonClick: () => void;\r\n onNextButtonClick: () => void;\r\n selectedIndex: number;\r\n scrollSnaps: number[];\r\n onDotButtonClick: (index: number) => void;\r\n scrollProgress: number;\r\n selectedSnap: number;\r\n snapCount: number;\r\n isScale: boolean;\r\n slidesArr: string[];\r\n setSlidesArr: React.Dispatch>;\r\n onThumbClick: (index: number) => void;\r\n carouselId: string;\r\n orientation: 'vertical' | 'horizontal';\r\n direction: 'ltr' | 'rtl' | undefined;\r\n handleKeyDown: (event: React.KeyboardEvent) => void;\r\n}\r\n\r\n// ============= CONTEXT =============\r\nconst CarouselContext = createContext(\r\n undefined\r\n);\r\n\r\nexport const useCarousel = () => {\r\n const context = useContext(CarouselContext);\r\n if (!context) {\r\n throw new Error('useCarousel must be used within a Carousel component');\r\n }\r\n return context;\r\n};\r\n\r\n// ============= UTILITIES =============\r\nconst TWEEN_FACTOR_BASE = 0.52;\r\nconst numberWithinRange = (number: number, min: number, max: number): number =>\r\n Math.min(Math.max(number, min), max);\r\n\r\n// ============= MAIN CAROUSEL COMPONENT =============\r\nexport const Carousel = forwardRef(\r\n (\r\n {\r\n children,\r\n options = {},\r\n plugins = [],\r\n className,\r\n isScale = false,\r\n dir,\r\n ...props\r\n },\r\n ref\r\n ) => {\r\n const carouselId = useId();\r\n const [slidesArr, setSlidesArr] = useState([]);\r\n\r\n const orientation = options.axis === 'y' ? 'vertical' : 'horizontal';\r\n const direction = options.direction ?? (dir as 'ltr' | 'rtl' | undefined);\r\n\r\n // Main carousel\r\n const [emblaRef, emblaApi] = useEmblaCarousel(\r\n {\r\n ...options,\r\n axis: orientation === 'vertical' ? 'y' : 'x',\r\n direction,\r\n },\r\n plugins\r\n );\r\n\r\n // Thumbnails carousel\r\n const [emblaThumbsRef, emblaThumbsApi] = useEmblaCarousel({\r\n containScroll: 'keepSnaps',\r\n dragFree: true,\r\n axis: orientation === 'vertical' ? 'y' : 'x',\r\n direction,\r\n });\r\n\r\n // State\r\n const [prevBtnDisabled, setPrevBtnDisabled] = useState(true);\r\n const [nextBtnDisabled, setNextBtnDisabled] = useState(true);\r\n const [selectedIndex, setSelectedIndex] = useState(0);\r\n const [scrollSnaps, setScrollSnaps] = useState([]);\r\n const [scrollProgress, setScrollProgress] = useState(0);\r\n const [snapCount, setSnapCount] = useState(0);\r\n\r\n // Navigation callbacks\r\n const onPrevButtonClick = useCallback(() => {\r\n emblaApi?.scrollPrev();\r\n }, [emblaApi]);\r\n\r\n const onNextButtonClick = useCallback(() => {\r\n emblaApi?.scrollNext();\r\n }, [emblaApi]);\r\n\r\n const onDotButtonClick = useCallback(\r\n (index: number) => {\r\n emblaApi?.scrollTo(index);\r\n },\r\n [emblaApi]\r\n );\r\n\r\n const onThumbClick = useCallback(\r\n (index: number) => {\r\n if (!emblaApi || !emblaThumbsApi) return;\r\n emblaApi.scrollTo(index);\r\n },\r\n [emblaApi, emblaThumbsApi]\r\n );\r\n\r\n // Keyboard navigation\r\n const handleKeyDown = useCallback(\r\n (event: React.KeyboardEvent) => {\r\n if (!emblaApi) return;\r\n switch (event.key) {\r\n case 'ArrowLeft':\r\n event.preventDefault();\r\n if (orientation === 'horizontal') {\r\n direction === 'rtl' ? onNextButtonClick() : onPrevButtonClick();\r\n }\r\n break;\r\n case 'ArrowRight':\r\n event.preventDefault();\r\n if (orientation === 'horizontal') {\r\n direction === 'rtl' ? onPrevButtonClick() : onNextButtonClick();\r\n }\r\n break;\r\n case 'ArrowUp':\r\n event.preventDefault();\r\n if (orientation === 'vertical') onPrevButtonClick();\r\n break;\r\n case 'ArrowDown':\r\n event.preventDefault();\r\n if (orientation === 'vertical') onNextButtonClick();\r\n break;\r\n }\r\n },\r\n [emblaApi, orientation, direction, onPrevButtonClick, onNextButtonClick]\r\n );\r\n\r\n // Selection handler\r\n const onSelect = useCallback(() => {\r\n if (!emblaApi) return;\r\n setSelectedIndex(emblaApi.selectedScrollSnap());\r\n setPrevBtnDisabled(!emblaApi.canScrollPrev());\r\n setNextBtnDisabled(!emblaApi.canScrollNext());\r\n emblaThumbsApi?.scrollTo(emblaApi.selectedScrollSnap());\r\n }, [emblaApi, emblaThumbsApi]);\r\n\r\n // Scroll progress handler\r\n const onScroll = useCallback((emblaApi: EmblaCarouselType) => {\r\n const progress = Math.max(0, Math.min(1, emblaApi.scrollProgress()));\r\n setScrollProgress(progress * 100);\r\n }, []);\r\n\r\n // Scale animation for isScale mode\r\n const tweenFactor = useRef(0);\r\n const tweenNodes = useRef([]);\r\n\r\n const setTweenNodes = useCallback(\r\n (emblaApi: EmblaCarouselType): void => {\r\n if (!isScale) return;\r\n tweenNodes.current = emblaApi\r\n .slideNodes()\r\n .map((slideNode) =>\r\n slideNode.querySelector('.slider_content')\r\n ) as HTMLElement[];\r\n },\r\n [isScale]\r\n );\r\n\r\n const setTweenFactor = useCallback(\r\n (emblaApi: EmblaCarouselType) => {\r\n if (!isScale) return;\r\n tweenFactor.current =\r\n TWEEN_FACTOR_BASE * emblaApi.scrollSnapList().length;\r\n },\r\n [isScale]\r\n );\r\n\r\n const tweenScale = useCallback(\r\n (emblaApi: EmblaCarouselType, eventName?: EmblaEventType) => {\r\n if (!isScale) return;\r\n const engine = emblaApi.internalEngine();\r\n const scrollProgress = emblaApi.scrollProgress();\r\n const slidesInView = emblaApi.slidesInView();\r\n const isScrollEvent = eventName === 'scroll';\r\n\r\n emblaApi.scrollSnapList().forEach((scrollSnap, snapIndex) => {\r\n let diffToTarget = scrollSnap - scrollProgress;\r\n const slidesInSnap = engine.slideRegistry[snapIndex];\r\n\r\n slidesInSnap.forEach((slideIndex) => {\r\n if (isScrollEvent && !slidesInView.includes(slideIndex)) return;\r\n\r\n if (engine.options.loop) {\r\n engine.slideLooper.loopPoints.forEach((loopItem) => {\r\n const target = loopItem.target();\r\n if (slideIndex === loopItem.index && target !== 0) {\r\n const sign = Math.sign(target);\r\n if (sign === -1) {\r\n diffToTarget = scrollSnap - (1 + scrollProgress);\r\n }\r\n if (sign === 1) {\r\n diffToTarget = scrollSnap + (1 - scrollProgress);\r\n }\r\n }\r\n });\r\n }\r\n\r\n const tweenValue = 1 - Math.abs(diffToTarget * tweenFactor.current);\r\n const scale = numberWithinRange(tweenValue, 0, 1).toString();\r\n const tweenNode = tweenNodes.current[slideIndex];\r\n if (tweenNode) {\r\n tweenNode.style.transform = `scale(${scale})`;\r\n }\r\n });\r\n });\r\n },\r\n [isScale]\r\n );\r\n\r\n // Effects\r\n useEffect(() => {\r\n if (!emblaApi) return;\r\n setScrollSnaps(emblaApi.scrollSnapList());\r\n setSnapCount(emblaApi.scrollSnapList().length);\r\n onSelect();\r\n onScroll(emblaApi);\r\n\r\n emblaApi\r\n .on('reInit', onSelect)\r\n .on('select', onSelect)\r\n .on('reInit', onScroll)\r\n .on('scroll', onScroll);\r\n\r\n if (isScale) {\r\n setTweenNodes(emblaApi);\r\n setTweenFactor(emblaApi);\r\n tweenScale(emblaApi);\r\n emblaApi\r\n .on('reInit', setTweenNodes)\r\n .on('reInit', setTweenFactor)\r\n .on('reInit', tweenScale)\r\n .on('scroll', tweenScale);\r\n }\r\n }, [\r\n emblaApi,\r\n onSelect,\r\n onScroll,\r\n isScale,\r\n setTweenNodes,\r\n setTweenFactor,\r\n tweenScale,\r\n ]);\r\n\r\n return (\r\n \r\n \r\n {children}\r\n \r\n \r\n );\r\n }\r\n);\r\n\r\nCarousel.displayName = 'Carousel';\r\n\r\n// ============= SLIDER CONTAINER =============\r\nexport const SliderContainer = forwardRef<\r\n HTMLDivElement,\r\n React.HTMLAttributes\r\n>(({ className, children, ...props }, ref) => {\r\n const { emblaRef, orientation } = useCarousel();\r\n\r\n return (\r\n
\r\n \r\n {children}\r\n
\r\n \r\n );\r\n});\r\n\r\nSliderContainer.displayName = 'SliderContainer';\r\n\r\n// ============= SLIDER ITEM =============\r\ninterface SliderProps extends React.HTMLAttributes {\r\n thumbnailSrc?: string;\r\n}\r\n\r\nexport const Slider = forwardRef(\r\n ({ children, className, thumbnailSrc, ...props }, ref) => {\r\n const { isScale, setSlidesArr, orientation } = useCarousel();\r\n\r\n useEffect(() => {\r\n if (thumbnailSrc) {\r\n setSlidesArr((prev) => {\r\n if (!prev.includes(thumbnailSrc)) {\r\n return [...prev, thumbnailSrc];\r\n }\r\n return prev;\r\n });\r\n }\r\n }, [thumbnailSrc, setSlidesArr]);\r\n\r\n return (\r\n \r\n {isScale ?
{children}
: children}\r\n \r\n );\r\n }\r\n);\r\n\r\nSlider.displayName = 'Slider';\r\n\r\n// ============= NAVIGATION BUTTONS =============\r\nexport const SliderPrevButton = forwardRef<\r\n HTMLButtonElement,\r\n React.ButtonHTMLAttributes\r\n>(({ children, className, ...props }, ref) => {\r\n const { onPrevButtonClick, prevBtnDisabled } = useCarousel();\r\n\r\n return (\r\n \r\n {children}\r\n \r\n );\r\n});\r\n\r\nSliderPrevButton.displayName = 'SliderPrevButton';\r\n\r\nexport const SliderNextButton = forwardRef<\r\n HTMLButtonElement,\r\n React.ButtonHTMLAttributes\r\n>(({ children, className, ...props }, ref) => {\r\n const { onNextButtonClick, nextBtnDisabled } = useCarousel();\r\n\r\n return (\r\n \r\n {children}\r\n \r\n );\r\n});\r\n\r\nSliderNextButton.displayName = 'SliderNextButton';\r\n\r\n// ============= PROGRESS BAR =============\r\nexport const SliderProgress = forwardRef<\r\n HTMLDivElement,\r\n React.HTMLAttributes\r\n>(({ className, ...props }, ref) => {\r\n const { scrollProgress } = useCarousel();\r\n\r\n return (\r\n \r\n \r\n \r\n );\r\n});\r\n\r\nSliderProgress.displayName = 'SliderProgress';\r\n\r\n// ============= SNAP DISPLAY =============\r\nexport const SliderSnapDisplay = forwardRef<\r\n HTMLDivElement,\r\n React.HTMLAttributes\r\n>(({ className, ...props }, ref) => {\r\n const { selectedSnap, snapCount } = useCarousel();\r\n const prevSnapRef = useRef(selectedSnap);\r\n const [direction, setDirection] = useState(0);\r\n\r\n useEffect(() => {\r\n setDirection(selectedSnap > prevSnapRef.current ? 1 : -1);\r\n prevSnapRef.current = selectedSnap;\r\n }, [selectedSnap]);\r\n\r\n return (\r\n \r\n \r\n ({ y: d * 20, opacity: 0 })}\r\n animate={{ y: 0, opacity: 1 }}\r\n // @ts-ignore\r\n exit={(d: number) => ({ y: d * -20, opacity: 0 })}\r\n >\r\n {selectedSnap + 1}\r\n \r\n \r\n / {snapCount}\r\n \r\n );\r\n});\r\n\r\nSliderSnapDisplay.displayName = 'SliderSnapDisplay';\r\n\r\n// ============= DOT BUTTONS =============\r\ninterface SliderDotButtonProps extends React.HTMLAttributes {\r\n activeClass?: string;\r\n}\r\n\r\nexport const SliderDotButton = forwardRef(\r\n ({ className, activeClass, ...props }, ref) => {\r\n const {\r\n selectedIndex,\r\n scrollSnaps,\r\n orientation,\r\n onDotButtonClick,\r\n carouselId,\r\n } = useCarousel();\r\n\r\n return (\r\n
\r\n {scrollSnaps.map((_, index) => (\r\n onDotButtonClick(index)}\r\n className={cn(\r\n 'relative inline-flex p-0 m-0',\r\n orientation === 'vertical' ? 'h-6 w-1' : 'w-6 h-1'\r\n )}\r\n >\r\n \r\n {index === selectedIndex && (\r\n \r\n \r\n \r\n )}\r\n \r\n ))}\r\n
\r\n );\r\n }\r\n);\r\n\r\nSliderDotButton.displayName = 'SliderDotButton';\r\n\r\n// ============= CAROUSEL INDICATORS =============\r\ninterface CarouselIndicatorProps\r\n extends React.ButtonHTMLAttributes {\r\n index: number;\r\n}\r\n\r\nexport const CarouselIndicator = forwardRef<\r\n HTMLButtonElement,\r\n CarouselIndicatorProps\r\n>(({ className, index, ...props }, ref) => {\r\n const { selectedIndex, onDotButtonClick } = useCarousel();\r\n const isActive = selectedIndex === index;\r\n\r\n return (\r\n onDotButtonClick(index)}\r\n className={cn(\r\n 'h-1.5 w-6 rounded-full transition-colors',\r\n isActive ? 'bg-primary' : 'bg-primary/50',\r\n className\r\n )}\r\n aria-label={`Go to slide ${index + 1}`}\r\n {...props}\r\n >\r\n Slide {index + 1}\r\n \r\n );\r\n});\r\n\r\nCarouselIndicator.displayName = 'CarouselIndicator';\r\n\r\n// Auto-generate thumbnails from slides\r\nexport const ThumbsSlider = forwardRef<\r\n HTMLDivElement,\r\n React.HTMLAttributes & {\r\n thumbsClassName?: string;\r\n thumbsSliderClassName?: string;\r\n }\r\n>(({ className, thumbsClassName, thumbsSliderClassName, ...props }, ref) => {\r\n const {\r\n slidesArr,\r\n selectedIndex,\r\n onThumbClick,\r\n orientation,\r\n emblaThumbsRef,\r\n } = useCarousel();\r\n\r\n if (slidesArr.length === 0) return null;\r\n\r\n return (\r\n \r\n \r\n {slidesArr.map((src, index) => (\r\n onThumbClick(index)}\r\n className={cn(\r\n 'shrink-0 cursor-pointer transition-opacity',\r\n 'border-2 rounded-md',\r\n orientation === 'vertical'\r\n ? 'basis-[15%] h-20'\r\n : 'basis-[15%] h-24',\r\n selectedIndex === index\r\n ? 'opacity-100 border-primary'\r\n : 'opacity-30 border-transparent',\r\n thumbsSliderClassName\r\n )}\r\n >\r\n \r\n \r\n ))}\r\n \r\n \r\n );\r\n});\r\n\r\nThumbsSlider.displayName = 'ThumbsSlider';\r\n\r\n// Alias for backward compatibility\r\n", "type": "registry:component" } ] }